{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# NodeJS\n",
    "\n",
    "## 1.环境配置\n",
    "\n",
    "<a href=\"http://www.cnblogs.com/dotnetcrazy/p/10061671.html#%E6%89%A9%E5%B1%95%E8%AF%B4%E6%98%8E\" target=\"_blank\">之前讲ES6的时候</a>有提过一部分Node的知识，简单回顾下：<a href=\"https://mp.weixin.qq.com/s?timestamp=1544749137&src=3&ver=1&signature=vnuwjSl1ABzrX1OgAvNfv5X2diylbeZHBka*ro35IujRIOq*bAyFNTFMYYDPrk35aT1VZTd0Auwoo9unagx2OtRhNv8Hr1tl8fCd41jFY2FRLilIWbvNNCIBHPUdcop8QA51NOUgGpXpf0W3toPFTsD9uMlKIww5SPtfpVGoDmg=\" target=\"_blank\">一文读懂ES6</a>\n",
    "\n",
    "### 1.1.NPM国内镜像\n",
    "\n",
    "**npm国内镜像**：`https://npm.taobao.org`\n",
    "\n",
    "配置国内源：**`npm install -g cnpm --registry=https://registry.npm.taobao.org`**\n",
    "\n",
    "然后就可以把`cnpm`当作`npm`来用了，比如之前的`React`组件案例：\n",
    "1. **`cnpm install react`**\n",
    "2. **`cnpm install react-dom`**\n",
    "3. **`cnpm i babel-core@old`**\n",
    "\n",
    "**卸载安装的包**：`npm uninstall -g uuid`用`npm`而不是`cnpm`\n",
    "\n",
    "常用参数说明：\n",
    "1. **`i`是`install`的简写**\n",
    "2. **`-g`是安装到全局环境中(默认是当前目录）**\n",
    "3. **`-D`添加为开发依赖（`-D ==> --save-dev` 开发环境）**\n",
    "4. **`-S`添加为生产依赖（`-S ==> --save` 生产环境）**\n",
    "    - eg：`cnpm i express -S`\n",
    "\n",
    "eg：`cnpm init`之后：\n",
    "![1.init.png](https://img2018.cnblogs.com/blog/1127869/201812/1127869-20181214214834607-1466037062.png)\n",
    "![1.add.png](https://img2018.cnblogs.com/blog/1127869/201812/1127869-20181214214930521-975989401.png)\n",
    "\n",
    "PS：**你把依赖包删了也没事，执行`cnpm i`就会会根据`package.json`自动安装依赖包**\n",
    "\n",
    "课外阅读：\n",
    "```\n",
    "npm如何管理依赖包的版本\n",
    "https://www.jianshu.com/p/1470c5d7b8c3\n",
    "\n",
    "禁止npm自动升级依赖包以及所有下级依赖包版本的方法\n",
    "https://www.jianshu.com/p/f481cf9b0817\n",
    "```\n",
    "\n",
    "---\n",
    "\n",
    "### 1.2.VSCode调试\n",
    "\n",
    "这个之前也说过，可以看看：<a href=\"http://www.cnblogs.com/dotnetcrazy/p/9962192.html#_map5\" target=\"_blank\">VSCode and NoteBook for JavaScript | NodeJS</a>，简单说下：\n",
    "\n",
    "每次F5运行的时候选一下`NodeJS`，或者添加一下调试的配置文件\n",
    "```json\n",
    "{\n",
    "    \"version\": \"0.2.0\",\n",
    "    \"configurations\": [\n",
    "        {\n",
    "            \"type\": \"node\",\n",
    "            \"request\": \"launch\",\n",
    "            \"name\": \"启动程序\",\n",
    "            \"program\": \"${workspaceFolder}/${relativeFile}\"\n",
    "        }\n",
    "    ]\n",
    "}\n",
    "```\n",
    "`nodejs`用法和`js`基本一样，只是多了些服务器的模块，eg：\n",
    "![1.vscode.png](https://img2018.cnblogs.com/blog/1127869/201812/1127869-20181212112409874-241695988.png)\n",
    "\n",
    "#### 配置NodeJS和HTML\n",
    "\n",
    "如果想同时运行`nodejs`和`html`再添加下配置就行了，比如：\n",
    "```json\n",
    "{\n",
    "    \"version\": \"0.2.0\",\n",
    "    \"configurations\": [\n",
    "        {\n",
    "            \"type\": \"chrome\",\n",
    "            \"request\": \"launch\",\n",
    "            \"name\": \"Launch Chrome against localhost\",\n",
    "            \"url\": \"http://localhost:8080/${relativeFile}\",\n",
    "            \"webRoot\": \"${workspaceFolder}\"\n",
    "        },\n",
    "        {\n",
    "            \"type\": \"node\",\n",
    "            \"request\": \"launch\",\n",
    "            \"name\": \"启动程序\",\n",
    "            \"program\": \"${workspaceFolder}/${relativeFile}\"\n",
    "        }\n",
    "    ]\n",
    "}\n",
    "```\n",
    "配置完成后：想运行HTML就选择谷歌浏览器\n",
    "![1.浏览器.png](https://img2018.cnblogs.com/blog/1127869/201812/1127869-20181212155241177-1408978964.png)\n",
    "\n",
    "想运行`nodejs`的`js`文件就选择启动程序（**现在运行JS文件的时候，F5即可自动切换成node**）\n",
    "![1.nodejs.png](https://img2018.cnblogs.com/blog/1127869/201812/1127869-20181212155336865-321252219.png)\n",
    "\n",
    "#### 配置说明\n",
    "\n",
    "1. `${workspaceRoot}` VS Code当前打开的文件夹\n",
    "2. `${file}` 当前打开的文件\n",
    "3. `${relativeFile}` 相对于`workspaceRoot`的相对路径\n",
    "4. `${fileBasename}` 当前打开文件的文件名\n",
    "5. `${fileDirname}` 所在的文件夹，是绝对路径\n",
    "6. `${fileExtname}` 当前打开文件的扩展名\n",
    "\n",
    "### 1.3.Jupyter NoteBook\n",
    "\n",
    "`ijavascript`依赖于`Python2.7`，详细过程：<a href=\"http://www.cnblogs.com/dotnetcrazy/p/9962192.html#_map7\" target=\"_blank\">Jupyter NoteBook IJavaScript 配置</a>\n",
    "\n",
    "```shell\n",
    "# 如果nodejs和npm没安装可以先安装下\n",
    "sudo apt-get install nodejs npm\n",
    "\n",
    "# 把ijavascript安装到全局环境中\n",
    "sudo npm install -g ijavascript\n",
    "\n",
    "#  安装\n",
    "ijsinstall\n",
    "```\n",
    "然后就和Python一样用了：\n",
    "![1.ijavascript.png](https://img2018.cnblogs.com/blog/1127869/201811/1127869-20181115105804115-1975534286.png)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2.常用模块\n",
    "\n",
    "这块官方文档写的很详细，我就简单说说，后面用到再详细说\n",
    "\n",
    "中文文档：<a href=\"http://nodejs.cn/api/\" target=\"_blank\">http://nodejs.cn/api</a>\n",
    "\n",
    "官方文档：<a href=\"https://nodejs.org/api/\" target=\"_blank\">https://nodejs.org/api</a>\n",
    "\n",
    "### 2.1.http（常用）\n",
    "\n",
    "文档：`http://nodejs.cn/api/http.html` or `https://nodejs.org/api/http.html`\n",
    "\n",
    "NodeJS既然作为服务器，那得先有个服务器的样子，我们来个简单的案例：\n",
    "```node\n",
    "// ES6语法：import http from \"http\"; （现在还没能完全支持）\n",
    "const http = require(\"http\")\n",
    "\n",
    "// 创建一个服务器\n",
    "let server = http.createServer((request, response) => {\n",
    "    // 每次请求都会执行这个方法\n",
    "    console.log(request.url);\n",
    "\n",
    "    response.write(\"<h1>Test NodeJS</h>\");\n",
    "    response.end() // 告诉浏览器响应结束\n",
    "\n",
    "});\n",
    "\n",
    "// 服务器启动并监听指定端口\n",
    "server.listen(8080); // 这个和其他语言不一样，直接监听对应的端口\n",
    "```\n",
    "效果：\n",
    "![1.vscode.png](https://img2018.cnblogs.com/blog/1127869/201812/1127869-20181212112409874-241695988.png)\n",
    "\n",
    "**其他内容需要结合其他模块一起讲解**\n",
    "\n",
    "### 2.2.fs（常用）\n",
    "\n",
    "文档：`http://nodejs.cn/api/fs.html` or `https://nodejs.org/api/fs.html`\n",
    "\n",
    "这个是IO对应的模块，推荐使用异步方法，简单看看：(`xxxFileSync`是同步方式，不建议使用)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "const fs = require(\"fs\");"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ReadFile异步验证]我出现就是异步\n",
      "{ Error: ENOENT: no such file or directory, open 'test.txt' errno: -2, code: 'ENOENT', syscall: 'open', path: 'test.txt' }\n"
     ]
    }
   ],
   "source": [
    "// 文件读取\n",
    "fs.readFile(\"test.txt\",(ex,data) => {\n",
    "    // 如果文件不存在就输出错误信息\n",
    "    if(ex){\n",
    "        console.log(ex);\n",
    "    }else{\n",
    "        console.log(data.toString());\n",
    "    }\n",
    "});\n",
    "\n",
    "console.log(\"[ReadFile异步验证]我出现就是异步\");"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[WriteFile异步验证]我出现就是异步\n"
     ]
    }
   ],
   "source": [
    "// 创建一个文件\n",
    "fs.writeFile(\"test.txt\",\"文本内容\",ex => {\n",
    "    if(ex){\n",
    "        // 出错就输出info\n",
    "        console.log(ex);\n",
    "    }\n",
    "});\n",
    "\n",
    "console.log(\"[WriteFile异步验证]我出现就是异步\");"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[AppendFile异步验证]我出现就是异步\n"
     ]
    }
   ],
   "source": [
    "// 文件追加\n",
    "fs.appendFile(\"test.txt\",\"追加内容\",ex => {\n",
    "   if(ex){\n",
    "        // 出错就输出info\n",
    "       console.log(ex);\n",
    "   } \n",
    "});\n",
    "\n",
    "console.log(\"[AppendFile异步验证]我出现就是异步\");"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "文本内容追加内容\n"
     ]
    }
   ],
   "source": [
    "// 现在再读着看看\n",
    "fs.readFile(\"test.txt\",(ex,data) => {\n",
    "    // 如果文件不存在就输出错误信息\n",
    "    if(ex){\n",
    "        console.log(ex);\n",
    "    }else{\n",
    "        console.log(data.toString());\n",
    "    }\n",
    "});"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "PS：如果文件不是文本文件，就不能`toString`了（`data`默认是`buffer`类型）\n",
    "\n",
    "```node\n",
    "fs.readFile(\"知识星球.png\", (ex, data) => {\n",
    "    if (ex) {\n",
    "        console.log(\"读取错误：\", ex);\n",
    "    } else {\n",
    "        console.log(data); // 看看buffer是啥样的\n",
    "        fs.writeFile(\"test.png\", data, ex => {\n",
    "            if (ex) {\n",
    "                console.log(\"复制错误：\", ex);\n",
    "            }\n",
    "        });\n",
    "    }\n",
    "});\n",
    "```\n",
    "效果：\n",
    "```\n",
    "<Buffer 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52 00 00 02 10 00 00 02 61 08 06 00 00 00 10 7e 89 ed 00 00 20 00 49 44 41 54 78 01 ec bd 07 98 64 57 79 ... >\n",
    "```\n",
    "图片照常打开：\n",
    "![公众号：逸鹏说道](https://img2018.cnblogs.com/blog/1127869/201809/1127869-20180915141424820-1572974566.png)\n",
    "\n",
    "简单小结一下：\n",
    "1. 读取文件：`fs.readFile(\"xxx\", (ex, data) => { });`\n",
    "3. 创建文件：`fs.writeFile(\"xxx\", data, ex => {});`\n",
    "3. 追加文件：`fs.appendFile(\"xxx\", data, ex => {});`\n",
    "4. `data`是`buffer`类型，内置了：\n",
    "    - `toString()`：buffer转换成字符串\n",
    "    - `toJSON()`：buffer转化成Json"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "scrolled": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<Buffer 5b 6f 62 6a 65 63 74 20 4f 62 6a 65 63 74 5d>\n",
      "{ type: 'Buffer',\n",
      "  data: [ 91, 111, 98, 106, 101, 99, 116, 32, 79, 98, 106, 101, 99, 116, 93 ] }\n",
      "[object Object]\n",
      "[object Object]\n"
     ]
    }
   ],
   "source": [
    "// 看个案例\n",
    "data = { \"name\": \"小明\", \"age\": \"23\" };\n",
    "\n",
    "fs.writeFile(\"to.txt\", data, ex => {\n",
    "    if (ex) {\n",
    "        console.log(ex);\n",
    "    }\n",
    "});\n",
    "\n",
    "fs.readFile(\"to.txt\", (ex, data) => {\n",
    "    if (ex) {\n",
    "        console.log(ex);\n",
    "    } else {\n",
    "        console.log(data);\n",
    "        console.log(data.toJSON());\n",
    "        console.log(data.toString());\n",
    "        console.log(data.toLocaleString());\n",
    "    }\n",
    "});"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### 注意点\n",
    "\n",
    "上面几个方法（eg：`readFile`）都是先把数据都缓存到内存中，然后才回调，这样比较浪费内存，对于大文件不友好，so ==> 流走起\n",
    "\n",
    "#### Stream（常用）\n",
    "\n",
    "用法比较简单，看个案例：\n",
    "```node\n",
    "const fs = require(\"fs\");\n",
    "\n",
    "let rs = fs.createReadStream(\"知识星球.png\");\n",
    "let ws = fs.createWriteStream(\"test.png\");\n",
    "// 可以这么理解，rs是水龙头防水的地方，写反了也就出不了水了\n",
    "rs.pipe(ws); // 创建一个管道，流从r端到w端\n",
    "```\n",
    "还有一些类似于监听的事件：\n",
    "```node\n",
    "const fs = require(\"fs\");\n",
    "\n",
    "let rs = fs.createReadStream(\"知识星球.png\");\n",
    "let ws = fs.createWriteStream(\"test.png\");\n",
    "rs.pipe(ws); // 创建一个管道，流从r端到w端\n",
    "\n",
    "// 可以理解为错误触发的事件\n",
    "rs.on(\"error\", ex => {\n",
    "    console.log(\"读取失败\", ex);\n",
    "});\n",
    "\n",
    "rs.on(\"end\", () => {\n",
    "    console.log(\"读取完成\");\n",
    "});\n",
    "\n",
    "ws.on(\"error\", ex => {\n",
    "    console.log(\"写入失败\", ex);\n",
    "});\n",
    "\n",
    "// 注意，写入流完成不叫end\n",
    "ws.on(\"finish\", () => {\n",
    "    console.log(\"写入完成\");\n",
    "});\n",
    "```\n",
    "\n",
    "#### fs扩展库\n",
    "\n",
    "推荐一个fs扩展库：`https://github.com/jprichardson/node-fs-extra`\n",
    "\n",
    "### 2.3.url（常用）\n",
    "\n",
    "文档：`http://nodejs.cn/api/url.html` or `https://nodejs.org/api/url.html`\n",
    "\n",
    "说`url`模块之前得先说下`querystring`模块\n",
    "\n",
    "#### 1.querystring\n",
    "\n",
    "文档：`http://nodejs.cn/api/querystring.html` or `https://nodejs.org/api/querystring.html`\n",
    "\n",
    "这个是专门针对参数进行解析的，来个案例："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "const querystring = require(\"querystring\");"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{ keyword: '空气净化器',\n",
      "  enc: 'utf-8',\n",
      "  qrst: '1',\n",
      "  rt: '1',\n",
      "  stop: '1',\n",
      "  vt: '2',\n",
      "  psort: '3',\n",
      "  stock: '1',\n",
      "  wtype: '1',\n",
      "  cod: '1',\n",
      "  click: '2' }\n"
     ]
    }
   ],
   "source": [
    "let jd_qs = \"keyword=空气净化器&enc=utf-8&qrst=1&rt=1&stop=1&vt=2&psort=3&stock=1&wtype=1&cod=1&click=2\";\n",
    "\n",
    "// 咋一看，好像挺方便，但是有坑：看下一个demo\n",
    "let str = querystring.parse(jd_qs);\n",
    "console.log(str)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{ 'https://search.jd.com/Search?keyword': '空气净化器',\n",
      "  enc: 'utf-8',\n",
      "  qrst: '1',\n",
      "  rt: '1',\n",
      "  stop: '1',\n",
      "  vt: '2',\n",
      "  psort: '3',\n",
      "  stock: '1',\n",
      "  wtype: '1',\n",
      "  cod: '1',\n",
      "  click: '2' }\n"
     ]
    }
   ],
   "source": [
    "// 用户请求一般都是类似于这样的\n",
    "let jd_url = \"https://search.jd.com/Search?keyword=空气净化器&enc=utf-8&qrst=1&rt=1&stop=1&vt=2&psort=3&stock=1&wtype=1&cod=1&click=2\";\n",
    "\n",
    "// querystring.parse 只是对?后面(不包括`?`)的参数进行解析（以`=`和`&`分隔）\n",
    "str = querystring.parse(jd_url);\n",
    "console.log(str);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "注意：**querystring.parse 只是对?后面(不包括`?`)的参数进行解析（以`=`和`&`分隔）**\n",
    "\n",
    "#### 2.url\n",
    "\n",
    "上面说下`querystring`只是一个铺垫，基本上不太用，`url`模块已经包含这个了："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "const url = require(\"url\");"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Url {\n",
      "  protocol: 'https:',\n",
      "  slashes: true,\n",
      "  auth: null,\n",
      "  host: 'search.jd.com',\n",
      "  port: null,\n",
      "  hostname: 'search.jd.com',\n",
      "  hash: null,\n",
      "  search: '?keyword=空气净化器&enc=utf-8&qrst=1&rt=1&stop=1&vt=2&psort=3&stock=1&wtype=1&cod=1&click=2',\n",
      "  query: 'keyword=空气净化器&enc=utf-8&qrst=1&rt=1&stop=1&vt=2&psort=3&stock=1&wtype=1&cod=1&click=2',\n",
      "  pathname: '/Search',\n",
      "  path: '/Search?keyword=空气净化器&enc=utf-8&qrst=1&rt=1&stop=1&vt=2&psort=3&stock=1&wtype=1&cod=1&click=2',\n",
      "  href: 'https://search.jd.com/Search?keyword=空气净化器&enc=utf-8&qrst=1&rt=1&stop=1&vt=2&psort=3&stock=1&wtype=1&cod=1&click=2' }\n"
     ]
    }
   ],
   "source": [
    "// port=null说明是默认端口（http：80，https：443）\n",
    "let jd_url = \"https://search.jd.com/Search?keyword=空气净化器&enc=utf-8&qrst=1&rt=1&stop=1&vt=2&psort=3&stock=1&wtype=1&cod=1&click=2\";\n",
    "\n",
    "let str = url.parse(jd_url);\n",
    "console.log(str); // 发现query并没有解析"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Url {\n",
      "  protocol: 'https:',\n",
      "  slashes: true,\n",
      "  auth: null,\n",
      "  host: 'search.jd.com',\n",
      "  port: null,\n",
      "  hostname: 'search.jd.com',\n",
      "  hash: null,\n",
      "  search: '?keyword=空气净化器&enc=utf-8&qrst=1&rt=1&stop=1&vt=2&psort=3&stock=1&wtype=1&cod=1&click=2',\n",
      "  query: \n",
      "   { keyword: '空气净化器',\n",
      "     enc: 'utf-8',\n",
      "     qrst: '1',\n",
      "     rt: '1',\n",
      "     stop: '1',\n",
      "     vt: '2',\n",
      "     psort: '3',\n",
      "     stock: '1',\n",
      "     wtype: '1',\n",
      "     cod: '1',\n",
      "     click: '2' },\n",
      "  pathname: '/Search',\n",
      "  path: '/Search?keyword=空气净化器&enc=utf-8&qrst=1&rt=1&stop=1&vt=2&psort=3&stock=1&wtype=1&cod=1&click=2',\n",
      "  href: 'https://search.jd.com/Search?keyword=空气净化器&enc=utf-8&qrst=1&rt=1&stop=1&vt=2&psort=3&stock=1&wtype=1&cod=1&click=2' }\n"
     ]
    }
   ],
   "source": [
    "// 想要解析`query`，可以多传一个参数\n",
    "str = url.parse(jd_url, true);\n",
    "console.log(str); // 对query解析"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "PS：一般都是这么用的：\n",
    "```node\n",
    "// {a, b} = {a:21,b=34,c=22} 只要对应即可解包，如果想取别名可以使用：{a:xx, b} = {...}\n",
    "let { pathname, query } = url.parse(request.url, true);\n",
    "```\n",
    "\n",
    "#### 3.前几个模块的综合案例\n",
    "\n",
    "结合上面的`HTTP模块`，来个简单的`web服务器`：\n",
    "\n",
    "```node\n",
    "const fs = require(\"fs\");\n",
    "const url = require(\"url\");\n",
    "const http = require(\"http\");\n",
    "\n",
    "// 创建服务\n",
    "let server = http.createServer((request, response) => {\n",
    "    // 请求\n",
    "    // {a, b} = {a:21,b=34,c=22} 只要对应即可解包，如果想取别名可以使用：{a:xx, b} = {...}\n",
    "    let { pathname, query } = url.parse(request.url, true);\n",
    "    console.log(query, pathname);\n",
    "\n",
    "    // 读取对应文件\n",
    "    fs.readFile(`www${pathname}`, (ex, data) => {\n",
    "        if (ex) {\n",
    "            // 返回404状态码，并设置编码为UTF-8\n",
    "            response.writeHeader(404, {\n",
    "                \"Content-Type\": \"text/html;charset=utf-8\"\n",
    "            });\n",
    "            // 提示需要在 writeHeader 之后，不然访问的是浏览器404页面\n",
    "            response.write(\"<h1>访问的页面不存在～</h1>\");\n",
    "        } else {\n",
    "            response.write(data);\n",
    "        }\n",
    "        // 响应结束\n",
    "        response.end();\n",
    "    });\n",
    "});\n",
    "// 服务器启动并监听指定端口\n",
    "server.listen(8080);\n",
    "```\n",
    "输出：(`www`目录就两个文件，一个`test.html`,一个`test.png`)\n",
    "\n",
    "![2.nodejs.png](https://img2018.cnblogs.com/blog/1127869/201812/1127869-20181214122914837-536774209.png)\n",
    "\n",
    "**推荐写法**：\n",
    "```node\n",
    "const fs = require(\"fs\");\n",
    "const url = require(\"url\");\n",
    "const http = require(\"http\");\n",
    "\n",
    "let server = http.createServer((request, response) => {\n",
    "    let { pathname } = url.parse(request.url, true);\n",
    "    console.log(pathname);\n",
    "\n",
    "    let rs = fs.createReadStream(`www${pathname}`);\n",
    "    // `request`和`response`就是一个典型的读写流（`ReadStream`、`WriteStream`）\n",
    "    rs.pipe(response);\n",
    "\n",
    "    // 读取失败 ==> 404\n",
    "    rs.on(\"error\", ex => {\n",
    "        response.writeHeader(404);\n",
    "        response.write(\"404 Not Found\");\n",
    "        response.end();\n",
    "    });\n",
    "});\n",
    "\n",
    "server.listen(8080);\n",
    "```\n",
    "\n",
    "PS：**`request`和`response`就是一个典型的读写流（`ReadStream`、`WriteStream`）**\n",
    "\n",
    "### 2.4.zlib（常用）\n",
    "\n",
    "文档：`http://nodejs.cn/api/zlib.html` or `https://nodejs.org/api/zlib.html`\n",
    "\n",
    "先看个案例：（`zlib`是读写流）\n",
    "```node\n",
    "const fs = require(\"fs\");\n",
    "const zlib = require(\"zlib\");\n",
    "\n",
    "// 读写流\n",
    "let gz = zlib.createGzip();\n",
    "// 读流\n",
    "let rs =fs.createReadStream(\"./www/jquery-2.1.1.js\");\n",
    "// 写流\n",
    "let ws =fs.createWriteStream(\"test.js.gz\");\n",
    "// 可以这么理解：（gz是读写流）\n",
    "// rs水龙头先传给了gz，gz又当一个水龙头传给了ws\n",
    "rs.pipe(gz).pipe(ws);\n",
    "\n",
    "ws.on(\"finish\",()=>{\n",
    "    console.log(\"写入完毕\");\n",
    "});\n",
    "```\n",
    "效果：\n",
    "![1.zlib.png](https://img2018.cnblogs.com/blog/1127869/201812/1127869-20181214234028423-1190004389.png)\n",
    "\n",
    "#### 静态服务器\n",
    "\n",
    "结合上面再来个加强版：\n",
    "```node\n",
    "const fs = require(\"fs\");\n",
    "const zlib = require(\"zlib\");\n",
    "const http = require(\"http\");\n",
    "\n",
    "let server = http.createServer((request, response) => {\n",
    "    // let { pathname } = url.parse(request.url);\n",
    "    console.log(request.url);\n",
    "    let rs = fs.createReadStream(`www${request.url}`);\n",
    "    let gz = zlib.createGzip();\n",
    "\n",
    "    // 响应之前告诉浏览器是gzip的格式\n",
    "    response.setHeader(\"Content-Encoding\", \"gzip\");\n",
    "    // 返回gzip压缩后的文件\n",
    "    rs.pipe(gz).pipe(response);\n",
    "\n",
    "    // 读取失败，404错误\n",
    "    rs.on(\"error\", ex => {\n",
    "        response.removeHeader(\"Content-Encoding\");\n",
    "        // 返回404状态码，并设置编码为UTF-8\n",
    "        response.writeHeader(404, {\n",
    "            \"Content-Type\": \"text/html;charset=utf-8\"\n",
    "        });\n",
    "        // 提示需要在 writeHeader 之后，不然访问的是浏览器404页面\n",
    "        response.write(\"<h2>您访问的页面不存在～</h2>\");\n",
    "        response.end();\n",
    "    });\n",
    "});\n",
    "\n",
    "server.listen(8080, () => {\n",
    "    console.log(\"服务器启动成功，端口：8080\");\n",
    "});\n",
    "```\n",
    "输出对比：\n",
    "![1.zlib2.png](https://img2018.cnblogs.com/blog/1127869/201812/1127869-20181215004614718-859741323.png)\n",
    "\n",
    "### 2.5.path\n",
    "\n",
    "文档：`http://nodejs.cn/api/path.html` or `https://nodejs.org/api/path.html`\n",
    "\n",
    "这个主要是针对路径的模块，看个案例：`path.parse()`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "const path = require(\"path\");"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "./images/png\n",
      "小明.png\n",
      ".png\n",
      "{ root: '',\n",
      "  dir: './images/png',\n",
      "  base: '小明.png',\n",
      "  ext: '.png',\n",
      "  name: '小明' }\n",
      "/home/dnt/桌面/work/BaseCode/javascript/NoteBook/2.Node\n",
      "/home/dnt/桌面/work/BaseCode/javascript/NoteBook/2.Node/images/png/小明.png\n"
     ]
    }
   ],
   "source": [
    "let file_name = \"./images/png/小明.png\";\n",
    "\n",
    "// 文件路径 ./images/png\n",
    "console.log(path.dirname(file_name));\n",
    "\n",
    "// 提取出用 `/` 隔开的 `path` 的最后一部分\n",
    "// 小明.png\n",
    "console.log(path.basename(file_name));\n",
    "\n",
    "// 文件后缀 .png\n",
    "console.log(path.extname(file_name));\n",
    "\n",
    "// 文件信息 {root: \"\", dir: \"./images/png\", base: \"小明.png\", ext: \".png\", name: \"小明\"}\n",
    "console.log(path.parse(file_name)); // 这个经常使用\n",
    "\n",
    "// ------------------------------\n",
    "// 当前文件所在文件夹绝对路径\n",
    "console.log(path.resolve());\n",
    "// 文件的绝对路径\n",
    "console.log(path.resolve(file_name));"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{ resolve: [Function: resolve],\n",
      "  normalize: [Function: normalize],\n",
      "  isAbsolute: [Function: isAbsolute],\n",
      "  join: [Function: join],\n",
      "  relative: [Function: relative],\n",
      "  _makeLong: [Function: _makeLong],\n",
      "  dirname: [Function: dirname],\n",
      "  basename: [Function: basename],\n",
      "  extname: [Function: extname],\n",
      "  format: [Function: format],\n",
      "  parse: [Function: parse],\n",
      "  sep: '/',\n",
      "  delimiter: ':',\n",
      "  win32: \n",
      "   { resolve: [Function: resolve],\n",
      "     normalize: [Function: normalize],\n",
      "     isAbsolute: [Function: isAbsolute],\n",
      "     join: [Function: join],\n",
      "     relative: [Function: relative],\n",
      "     _makeLong: [Function: _makeLong],\n",
      "     dirname: [Function: dirname],\n",
      "     basename: [Function: basename],\n",
      "     extname: [Function: extname],\n",
      "     format: [Function: format],\n",
      "     parse: [Function: parse],\n",
      "     sep: '\\\\',\n",
      "     delimiter: ';',\n",
      "     win32: [Circular],\n",
      "     posix: [Circular] },\n",
      "  posix: [Circular] }\n"
     ]
    }
   ],
   "source": [
    "// 可以看看path的完整信息\n",
    "console.log(path);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2.6.crypto\n",
    "\n",
    "文档：`http://nodejs.cn/api/crypto.html` or `https://nodejs.org/api/crypto.html`\n",
    "\n",
    "这块主要就是用来加密（签名）的，很简单："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "const cypto = require(\"crypto\");"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "e10adc3949ba59abbe56e057f20f883e\n"
     ]
    }
   ],
   "source": [
    "let obj = cypto.createHash(\"md5\");\n",
    "// // PS：分多次提交也一样：\n",
    "// obj.update(\"123\");\n",
    "// obj.update(\"456\");\n",
    "obj.update(\"123456\");\n",
    "// e10adc3949ba59abbe56e057f20f883e\n",
    "console.log(obj.digest(\"hex\"));"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "7c4a8d09ca3762af61e59520943dc26494f8941b\n"
     ]
    }
   ],
   "source": [
    "let obj2 = cypto.createHash(\"sha1\");\n",
    "obj2.update(\"123456\");\n",
    "// 7c4a8d09ca3762af61e59520943dc26494f8941b\n",
    "console.log(obj2.digest(\"hex\"));"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92\n"
     ]
    }
   ],
   "source": [
    "let obj3 = cypto.createHash(\"sha256\");\n",
    "obj3.update(\"123456\");\n",
    "// 8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92\n",
    "console.log(obj3.digest(\"hex\"));"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2.7.os\n",
    "\n",
    "文档：`http://nodejs.cn/api/os.html` or `https://nodejs.org/api/os.html`\n",
    "\n",
    "这块是系统相关的模块，看个常用的案例："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "scrolled": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "4\n",
      "[ { model: 'Intel(R) Core(TM) i5-3210M CPU @ 2.50GHz',\n",
      "    speed: 2804,\n",
      "    times: { user: 6287300, nice: 2400, sys: 1690400, idle: 80994500, irq: 0 } },\n",
      "  { model: 'Intel(R) Core(TM) i5-3210M CPU @ 2.50GHz',\n",
      "    speed: 2661,\n",
      "    times: { user: 5714300, nice: 2800, sys: 1585700, idle: 83027300, irq: 0 } },\n",
      "  { model: 'Intel(R) Core(TM) i5-3210M CPU @ 2.50GHz',\n",
      "    speed: 2796,\n",
      "    times: { user: 6217400, nice: 2700, sys: 1720400, idle: 82200300, irq: 0 } },\n",
      "  { model: 'Intel(R) Core(TM) i5-3210M CPU @ 2.50GHz',\n",
      "    speed: 2821,\n",
      "    times: { user: 5779100, nice: 1900, sys: 1589800, idle: 82863800, irq: 0 } } ]\n"
     ]
    }
   ],
   "source": [
    "const os = require(\"os\");\n",
    "\n",
    "// cup核数\n",
    "console.log(os.cpus().length);\n",
    "\n",
    "// 输出我老电脑CPU详细信息\n",
    "console.log(os.cpus());"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2.8.多进程\n",
    "\n",
    "可以参考我之前讲的Python多进程进行对比学习：<https://www.cnblogs.com/dotnetcrazy/p/9363810.html>\n",
    "\n",
    "#### 基础入门\n",
    "\n",
    "```node\n",
    "const os = require(\"os\");\n",
    "const process = require(\"process\");\n",
    "const cluster = require(\"cluster\");\n",
    "\n",
    "// 主进程：分配任务\n",
    "if (cluster.isMaster) {\n",
    "    console.log(`主进程PID：${process.pid}`);\n",
    "    for (let i = 0; i < os.cpus().length; i++) {\n",
    "        cluster.fork();\n",
    "    }\n",
    "} else {\n",
    "    // 子进程执行任务\n",
    "    console.log(`当前进程PID：${process.pid}，父进程PPID：${process.ppid}`);\n",
    "}\n",
    "```\n",
    "输出：\n",
    "```\n",
    "主进程PID：20680\n",
    "当前进程PID：20620，父进程PPID：20680\n",
    "当前进程PID：23340，父进程PPID：20680\n",
    "当前进程PID：11644，父进程PPID：20680\n",
    "当前进程PID：22144，父进程PPID：20680\n",
    "```\n",
    "\n",
    "#### 服务器的案例\n",
    "```node\n",
    "const os = require(\"os\");\n",
    "const http = require(\"http\");\n",
    "const process = require(\"process\");\n",
    "const cluster = require(\"cluster\");\n",
    "\n",
    "// 主进程：分配任务\n",
    "if (cluster.isMaster) {\n",
    "    console.log(`主进程PID：${process.pid}`);\n",
    "    for (let i = 0; i < os.cpus().length; i++) {\n",
    "        cluster.fork();\n",
    "    }\n",
    "} else {\n",
    "    // 子进程执行任务\n",
    "    http.createServer((request, response) => {\n",
    "        console.log(`当前进程PID：${process.pid}，父进程PPID：${process.ppid}`);\n",
    "        response.write(\"Fork Test\");\n",
    "        response.end();\n",
    "    }).listen(8080, () => {\n",
    "        console.log(`服务器启动成功，当前端口：8080，进程PID：${process.pid}`);\n",
    "    });\n",
    "}\n",
    "```\n",
    "输出：\n",
    "![2.process.png](https://img2018.cnblogs.com/blog/1127869/201812/1127869-20181215233123179-1082259632.png)\n",
    "\n",
    "#### 注意点\n",
    "\n",
    "PS：NodeJS注意下面几地方：\n",
    "1. **子进程可以共用一个端口**\n",
    "2. **并发的时候，如果不等到某个核的CPU100%，其他核的CPU就看戏（不工作）**\n",
    "\n",
    "### 2.9.uuid\n",
    "\n",
    "这个是第三方模块，如果没安装：\n",
    "1. `cnpm init`\n",
    "2. `cnpm i uuid -D`（开发环境） or `cnpm i uuid -S`（生产环境）\n",
    "\n",
    "![1.init.png](https://img2018.cnblogs.com/blog/1127869/201812/1127869-20181214214834607-1466037062.png)\n",
    "![1.add.png](https://img2018.cnblogs.com/blog/1127869/201812/1127869-20181214214930521-975989401.png)\n",
    "\n",
    "使用案例：\n",
    "```node\n",
    "const uuid = require(\"uuid/v4\");\n",
    "\n",
    "let uid = uuid();\n",
    "console.log(uid);\n",
    "```\n",
    "输出：\n",
    "```\n",
    "65334387-110e-489a-b4c5-cb19cb3875d0\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3.Web专栏\n",
    "\n",
    "参考文档：<a href=\"https://developer.mozilla.org/zh-CN/docs/Web/HTTP\" target=\"_blank\">https://developer.mozilla.org/zh-CN/docs/Web/HTTP</a>\n",
    "\n",
    "### 1.页面缓存\n",
    "\n",
    "参考链接：<a href=\"https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers\" target=\"_blank\">https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers</a>\n",
    "\n",
    "先看个知识点：查看文件最后一次修改时间：\n",
    "```node\n",
    "const fs = require(\"fs\");\n",
    "\n",
    "fs.stat(\"www/test.html\", (ex, stats) => {\n",
    "    if (ex) {\n",
    "        console.log(ex);\n",
    "    } else {\n",
    "        // mtime：最后一次修改时间\n",
    "        console.log(stats.mtime);\n",
    "        \n",
    "        // UTC：世界统一时间：GMT ==> UTC\n",
    "        console.log(stats.mtime.toUTCString());\n",
    "        console.log(stats.mtime.toGMTString());\n",
    "    }\n",
    "});\n",
    "```\n",
    "输出：\n",
    "```\n",
    "Fri Dec 14 2018 23:40:42 GMT+0800 (GMT+08:00) {}\n",
    "Fri, 14 Dec 2018 15:40:42 GMT\n",
    "Fri, 14 Dec 2018 15:40:42 GMT\n",
    "```\n",
    "\n",
    "简单说下**HTTP状态码**：（这边说的页面缓存就是指的304重定向）\n",
    "1. **20x系列**：服务器正常响应\n",
    "2. **30x系列**：重定向\n",
    "    - 301：代表永久重定向，浏览器下次访问这个页面就直接去目的url了（不推荐）\n",
    "    - **302**：临时重定向，项目升级之后Url经常变，这个302经常用\n",
    "        - eg:访问`baidu.com` =302=> `www.baidu.com`\n",
    "    - **304**：这个是重定向到本地缓存\n",
    "        - 服务器文件没更新，浏览器就直接访问本地缓存了\n",
    "3. **40x系列**：一般都是客户端请求有问题\n",
    "    - eg: `404 not found`\n",
    "4. **50x系列**：一般都是服务端出问题了\n",
    "    - eg：`500 Server Error`\n",
    "\n",
    "大概流程：\n",
    "1. 浏览器请求页面，服务器响应（200），并返回一个`Last-Modified`的`Response Header`\n",
    "2. 浏览器再次请求页面，发送一个`If-Modified-Since`的`Request Header`\n",
    "    - 服务器页面修改时间 > 浏览器的页面修改时间：** 继续第一步操作 **\n",
    "    - 服务器页面没修改：服务端返回`304`本地文件重定向，浏览器读取本地页面\n",
    "\n",
    "#### 带缓存的HTTPServer\n",
    "\n",
    "看一个带缓存的HTTPServer\n",
    "```node\n",
    "const fs = require(\"fs\");\n",
    "const url = require(\"url\");\n",
    "const http = require(\"http\");\n",
    "\n",
    "http.createServer((request, response) => {\n",
    "    let { pathname } = url.parse(request.url);\n",
    "    let file_path = `www${pathname}`;\n",
    "    console.log(pathname);\n",
    "\n",
    "    // 提炼一个404响应方法\n",
    "    let write_404 = () => {\n",
    "        response.writeHeader(404);\n",
    "        response.write(\"404 Not Found\");\n",
    "        response.end();\n",
    "    }\n",
    "\n",
    "    // 文件状态\n",
    "    fs.stat(file_path, (ex, stats) => {\n",
    "        if (ex) {\n",
    "            write_404();\n",
    "        } else {\n",
    "            // 封装一个正常响应的方法\n",
    "            let write_html = () => {\n",
    "                // 不管啥情况，反正要返回`Last-Modified`\n",
    "                response.setHeader(\"Last-Modified\", stats.mtime.toUTCString());\n",
    "                var rs = fs.createReadStream(file_path);\n",
    "                rs.pipe(response);\n",
    "\n",
    "                // 文件读取如果出错，就返回404\n",
    "                rs.on(\"error\", ex => {\n",
    "                    write_404();\n",
    "                });\n",
    "            }\n",
    "\n",
    "            // 看看浏览器有没有发送`If-Modified-Since`的请求头\n",
    "            // 必须小写，不然不能解析（接口是小写）\n",
    "            // console.log(req.headers['If-Modified-Since']);\n",
    "            // console.log(req.headers['if-modified-since']);\n",
    "            let client_time = request.headers[\"if-modified-since\"];\n",
    "            // 说明之前访问过，现在是浏览器再次请求\n",
    "            if (client_time) {\n",
    "                // 易错：比较毫秒（getTime）会有问题，转换成`秒`并取整（Math.floor:返回小于或等于其数值参数的最大整数）\n",
    "                client_time = Math.floor(new Date(client_time).getTime() / 1000);\n",
    "                let server_time = Math.floor(stats.mtime.getTime() / 1000);\n",
    "                // 如果页面修改过\n",
    "                if (server_time > client_time) {\n",
    "                    console.log(\"页面修改过\");\n",
    "                    write_html();\n",
    "                } else {\n",
    "                    console.log(\"304\");\n",
    "                    // 没有修改就返回304，让浏览器读本地文件\n",
    "                    response.writeHeader(304);\n",
    "                    response.end();\n",
    "                }\n",
    "            } else {\n",
    "                // 正常的页面请求（200）\n",
    "                console.log(\"200\");\n",
    "                write_html();\n",
    "            }\n",
    "        }\n",
    "    });\n",
    "}).listen(8080, () => {\n",
    "    console.log(\"服务器成功启动，端口：8080\");\n",
    "});\n",
    "```\n",
    "输出：\n",
    "\n",
    "![3.cache1.png](https://img2018.cnblogs.com/blog/1127869/201812/1127869-20181219114651862-816200948.png)\n",
    "\n",
    "![3.cache2.png](https://img2018.cnblogs.com/blog/1127869/201812/1127869-20181219114850265-877554642.png)\n",
    "\n",
    "\n",
    "PS：如果你看不到304状态码，可以看看控制台设置：\n",
    "\n",
    "![3.cache3.png](https://img2018.cnblogs.com/blog/1127869/201812/1127869-20181219114953896-682259557.png)\n",
    "\n",
    "![3.cache4.png](https://img2018.cnblogs.com/blog/1127869/201812/1127869-20181219114934051-15520356.png)\n",
    "\n",
    "\n",
    "再次归纳一下**缓存实现过程**：\n",
    "1. 第一次：服务器给浏览器发送：`Last-Modified: Fri, 14 Dec 2018 15:40:42 GMT`\n",
    "2. 第二次：服务器给浏览器发送：`If-Modified-Since: Fri, 14 Dec 2018 15:40:42 GMT`\n",
    "3. 第三次：服务器给浏览器发送：304\n",
    "\n",
    "有几个坑注意下：\n",
    "1. `request.headers[\"if-modified-since\"]`\n",
    "    - `if-modified-since`必须是小写（接口里面的key都是小写）\n",
    "2. 比较毫秒（getTime）会有问题，建议转换成`秒`并取整\n",
    "    - `Math.floor(data.getTime()/1000)`\n",
    "\n",
    "PS：必须是小写的说明（接口源码）\n",
    "```ts\n",
    "interface IncomingHttpHeaders {\n",
    "    'accept'?: string;\n",
    "    'access-control-allow-origin'?: string;\n",
    "    'access-control-allow-credentials'?: string;\n",
    "    'access-control-expose-headers'?: string;\n",
    "    'access-control-max-age'?: string;\n",
    "    'access-control-allow-methods'?: string;\n",
    "    'access-control-allow-headers'?: string;\n",
    "    'accept-patch'?: string;\n",
    "    'accept-ranges'?: string;\n",
    "    'age'?: string;\n",
    "    'allow'?: string;\n",
    "    'alt-svc'?: string;\n",
    "    'authorization'?: string;\n",
    "    'cache-control'?: string;\n",
    "    'connection'?: string;\n",
    "    'content-disposition'?: string;\n",
    "    'content-encoding'?: string;\n",
    "    'content-language'?: string;\n",
    "    'content-length'?: string;\n",
    "    'content-location'?: string;\n",
    "    'content-range'?: string;\n",
    "    'content-type'?: string;\n",
    "    'cookie'?: string;\n",
    "    'date'?: string;\n",
    "    'expect'?: string;\n",
    "    'expires'?: string;\n",
    "    'forwarded'?: string;\n",
    "    'from'?: string;\n",
    "    'host'?: string;\n",
    "    'if-match'?: string;\n",
    "    'if-modified-since'?: string;\n",
    "    'if-none-match'?: string;\n",
    "    'if-unmodified-since'?: string;\n",
    "    'last-modified'?: string;\n",
    "    'location'?: string;\n",
    "    'pragma'?: string;\n",
    "    'proxy-authenticate'?: string;\n",
    "    'proxy-authorization'?: string;\n",
    "    'public-key-pins'?: string;\n",
    "    'range'?: string;\n",
    "    'referer'?: string;\n",
    "    'retry-after'?: string;\n",
    "    'set-cookie'?: string[];\n",
    "    'strict-transport-security'?: string;\n",
    "    'trailer'?: string;\n",
    "    'transfer-encoding'?: string;\n",
    "    'tk'?: string;\n",
    "    'upgrade'?: string;\n",
    "    'user-agent'?: string;\n",
    "    'vary'?: string;\n",
    "    'via'?: string;\n",
    "    'warning'?: string;\n",
    "    'www-authenticate'?: string;\n",
    "    [header: string]: string | string[] | undefined;\n",
    "}\n",
    "```\n",
    "\n",
    "PS：使用 Last-Modified 存在一些弊端，这其中最常见的就是这样两个场景：(`Etag`一般用不到)\n",
    "1. 我们编辑了文件，但文件的内容没有改变。服务端并不清楚我们是否真正改变了文件，它仍然通过最后编辑时间进行判断。因此这个资源在再次被请求时，会被当做新资源，进而引发一次完整的响应——不该重新请求的时候，也会重新请求。\n",
    "2. 当我们修改文件的速度过快时（比如花了 100ms 完成了改动），由于 If-Modified-Since 只能检查到以秒为最小计量单位的时间差，所以它是感知不到这个改动的——该重新请求的时候，反而没有重新请求了。\n",
    "\n",
    "这两个场景其实指向了同一个`bug`==>服务器并没有正确感知文件的变化，为了解决这样的问题**`Etag`**作为`Last-Modified`的补充出现了。\n",
    "\n",
    "**Etag 是由服务器为每个资源生成的唯一的标识字符串，这个标识字符串是基于文件内容编码的，只要文件内容不同，它们对应的`Etag`就是不同的，反之亦然。因此`Etag`能够精准地感知文件的变化**。`Etag`和`Last-Modified`类似，当首次请求时，我们会在响应头里获取到一个最初的标识符字符串\n",
    "\n",
    "举个例子，它可以是这样的：**`ETag: W/\"2a3b-1602480f459\"`**\n",
    "\n",
    "那么下一次请求时，请求头里就会带上一个值相同的、名为`if-None-Match`的字符串供服务端比对了：`If-None-Match: W/\"2a3b-1602480f459\"`\n",
    "\n",
    "`Etag`的生成过程需要服务器额外付出开销，会影响服务端的性能，这是它的弊端。因此启用`Etag`需要看看实际的需求（`Etag`并不能替代`Last-Modified`，它只能作为`Last-Modified`的补充\n",
    "\n",
    "PS：**当`Etag`和`Last-Modified`同时存在时，以`Etag`为准**\n",
    "\n",
    "关于缓存的其他内容我推荐一篇文章：<a href=\"https://juejin.im/book/5b936540f265da0a9624b04b/section/5b9ba651f265da0ac726e5de\" target=\"_blank\">浏览器缓存机制介绍与缓存策略剖析</a>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2.xxxx\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Javascript (Node.js)",
   "language": "javascript",
   "name": "javascript"
  },
  "language_info": {
   "file_extension": ".js",
   "mimetype": "application/javascript",
   "name": "javascript",
   "version": "8.10.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
